using GameFrame.Deterministic;
using GameFrame.Core;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace GameFrame.Physics2D
{
  /// <summary>
  /// Information returned from a valid hit of a physics query.
  /// </summary>
  /// 
  ///             \ingroup Physics2dApi
  [StructLayout(LayoutKind.Explicit)]
  public struct Hit
  {
    public const int SIZE = 64;
    [FieldOffset(0)]
    private ushort _isTrigger;
    /// <summary>
    /// The <see cref="F:Quantum.Shape2D.UserTag" /> field of the shape hit by the query.
    /// </summary>
    [FieldOffset(2)]
    public ushort ShapeUserTag;
    [FieldOffset(4)]
    internal int _staticColliderIndex;
    private const int PenetrationOffset = 8;
    /// <summary>
    /// The penetration of a shape overlap query on the collider that was hit.
    /// <remarks>
    /// This field is not computed unless <see cref="F:Quantum.QueryOptions.ComputeDetailedInfo" /> is used in the query options
    /// and only applies to Shape Overlap queries.
    /// It overlaps in memory and is mutually exclusive with <see cref="F:Quantum.Physics2D.Hit.CastDistanceNormalized" />.
    /// </remarks>
    /// </summary>
    [FieldOverlap(8)]
    [FieldOffset(8)]
    public FP OverlapPenetration;
    /// <summary>
    /// A normalized value (0 to 1) of the feature being casted when the hit occurs, relative to its full cast distance.
    /// <remarks>
    /// This field only applies to Ray, Line or Shape casts.
    /// It overlaps in memory and is mutually exclusive with <see cref="F:Quantum.Physics2D.Hit.OverlapPenetration" />.
    /// </remarks>
    /// </summary>
    [FieldOverlap(8)]
    [FieldOffset(8)]
    public FP CastDistanceNormalized;
    private const int EntityOffset = 16;
    [FieldOverlap(16)]
    [FieldOffset(16)]
    internal EntityRef _entity;
    [FieldOverlap(16)]
    [FieldOffset(16)]
    internal AssetGuid _mapGuid;
    [FieldOffset(24)]
    internal long sqrDistance;
    private const int FirstVector2 = 32;
    /// <summary>
    /// The point in world space where the collider is hit by the query.
    /// </summary>
    [FieldOffset(32)]
    public FPVector2 Point;
    /// <summary>
    /// The surface normal of the hit.
    /// <remarks>
    /// This field is not computed unless <see cref="F:Quantum.QueryOptions.ComputeDetailedInfo" /> is used in the query options.
    /// </remarks>
    /// </summary>
    [FieldOffset(48)]
    public FPVector2 Normal;

    /// <summary>
    /// If the collider hit by the query is not static, i.e. it belongs to an <see cref="P:Quantum.Physics2D.Hit.Entity" />.
    /// </summary>
    public bool IsDynamic
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this._staticColliderIndex < 0;
    }

    /// <summary>
    /// 0-based index of the static collider on the <see cref="F:Quantum.Map.StaticColliders2D" /> array of the map referenced by <see cref="P:Quantum.Physics2D.Hit.StaticColliderMapRef" />.
    /// If the hit <see cref="P:Quantum.Physics2D.Hit.IsDynamic" />, this value is -1.
    /// Tip: use <see cref="M:Quantum.Physics2D.Hit.TryGetStaticCollider(Quantum.Core.FrameBase,Quantum.MapStaticCollider2D@)" />, <see cref="M:Quantum.Physics2D.Hit.TryGetStaticData(Quantum.Core.FrameBase,Quantum.StaticColliderData@)" />, <see cref="M:Quantum.Physics2D.Hit.GetStaticCollider(Quantum.Core.FrameBase)" /> or <see cref="M:Quantum.Physics2D.Hit.GetStaticData(Quantum.Core.FrameBase)" />
    /// in order to safely and conveniently access info about a static collider hit.
    /// </summary>
    public int StaticColliderIndex
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this._staticColliderIndex;
    }

    /// <summary>
    /// The reference to the map used by the physics engine when a static collider is hit.
    /// Otherwise, this value is default (hit <see cref="P:Quantum.Physics2D.Hit.IsDynamic" />).
    /// </summary>
    public AssetRefMap StaticColliderMapRef
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get
      {
        if (this._staticColliderIndex < 0)
          return new AssetRefMap();
        return new AssetRefMap() { Id = this._mapGuid };
      }
    }

    /// <summary>If the collider hit by the query is Trigger.</summary>
    public bool IsTrigger
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this._isTrigger == (ushort) 1;
      [MethodImpl(MethodImplOptions.AggressiveInlining)] set => this._isTrigger = value ? (ushort) 1 : (ushort) 0;
    }

    /// <summary>
    /// The reference of the entity hit by the query, <see cref="P:Quantum.EntityRef.None" /> if hitting a static collider (<see cref="P:Quantum.Physics2D.Hit.IsDynamic" /> false).
    /// </summary>
    public EntityRef Entity
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this._staticColliderIndex >= 0 ? new EntityRef() : this._entity;
    }

    [Obsolete("Renamed to 'OverlapPenetration'.")]
    public FP Penetration
    {
      get => this.OverlapPenetration;
      set => this.OverlapPenetration = value;
    }

    [Obsolete("Renamed to 'CastDistanceNormalized'.")]
    public FP CastDistance
    {
      get => this.CastDistanceNormalized;
      set => this.CastDistanceNormalized = value;
    }

    /// <summary>
    /// Sets the entity that was hit. Automatically resets <see cref="P:Quantum.Physics2D.Hit.StaticColliderIndex" /> to -1.
    /// </summary>
    /// <param name="entity">The entity reference.</param>
    public void SetHitEntity(EntityRef entity)
    {
      this._entity = entity;
      this._staticColliderIndex = -1;
    }

    /// <summary>
    /// Sets the index of the static collider hit on the <see cref="F:Quantum.Map.StaticColliders2D" /> array of the referenced map.
    /// </summary>
    /// <param name="staticColliderIndex">A valid index of the static collider on the array of the map.</param>
    /// <param name="mapRef">A reference to the map where the static collider hit is located.</param>
    public void SetHitStaticCollider(int staticColliderIndex, AssetRefMap mapRef)
    {
      this._staticColliderIndex = staticColliderIndex;
      this._mapGuid = mapRef.Id;
    }

    /// <summary>Tries to access the static collider that was hit.</summary>
    /// <param name="f">Current frame.</param>
    /// <param name="collider">A copy of the static collider data found. Default if the metadata is not valid (returns false).</param>
    /// <returns>False if the collider hit is not static or the collide index or referenced map are not valid. True otherwise.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryGetStaticCollider(FrameBase f, out MapStaticCollider2D collider) => this.TryGetStaticCollider((FrameThreadSafe) f, out collider);

    public bool TryGetStaticCollider(FrameThreadSafe f, out MapStaticCollider2D collider)
    {
      Map asset;
      if (this._staticColliderIndex < 0 || !f.TryFindAsset<Map>(this._mapGuid, out asset) || this._staticColliderIndex >= asset.StaticColliders2D.Length)
      {
        collider = new MapStaticCollider2D();
        return false;
      }
      collider = asset.StaticColliders2D[this._staticColliderIndex];
      return true;
    }

    /// <summary>
    /// Tries to access the data of the static collider that was hit (<see cref="F:Quantum.MapStaticCollider2D.StaticData" />).
    /// </summary>
    /// <param name="f">Current frame.</param>
    /// <param name="data">A copy of the static collider data found. Default if the metadata is not valid (returns false).</param>
    /// <returns>False if the collider hit is not static or the collide index or referenced map are not valid. True otherwise.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryGetStaticData(FrameBase f, out StaticColliderData data) => this.TryGetStaticData((FrameThreadSafe) f, out data);

    public bool TryGetStaticData(FrameThreadSafe f, out StaticColliderData data)
    {
      Map asset;
      if (this._staticColliderIndex < 0 || !f.TryFindAsset<Map>(this._mapGuid, out asset) || this._staticColliderIndex >= asset.StaticColliders2D.Length)
      {
        data = new StaticColliderData();
        return false;
      }
      data = asset.StaticColliders2D[this._staticColliderIndex].StaticData;
      return true;
    }

    /// <summary>
    /// Gets a reference to the static collider hit. Does not perform additional safety checks.
    /// </summary>
    /// <param name="f">Current frame.</param>
    /// <exception cref="T:System.NullReferenceException">If the <see cref="P:Quantum.Physics2D.Hit.StaticColliderMapRef" /> does not reference a valid asset.</exception>
    /// <exception cref="T:System.IndexOutOfRangeException">If the hit is not static (<see cref="P:Quantum.Physics2D.Hit.IsDynamic" />) or the collider index is not valid for the array of static colliders on the referenced map.</exception>
    /// <returns>A reference to the static collider hit.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ref MapStaticCollider2D GetStaticCollider(FrameBase f) => ref this.GetStaticCollider((FrameThreadSafe) f);

    public ref MapStaticCollider2D GetStaticCollider(FrameThreadSafe f) => ref f.FindAsset<Map>(this._mapGuid).StaticColliders2D[this._staticColliderIndex];

    /// <summary>
    /// Gets a reference to the data of the static collider hit (<see cref="F:Quantum.MapStaticCollider2D.StaticData" />). Does not perform additional safety checks.
    /// </summary>
    /// <param name="f">Current frame.</param>
    /// <exception cref="T:System.NullReferenceException">If the <see cref="P:Quantum.Physics2D.Hit.StaticColliderMapRef" /> does not reference a valid asset.</exception>
    /// <exception cref="T:System.IndexOutOfRangeException">If the hit is not static (<see cref="P:Quantum.Physics2D.Hit.IsDynamic" />) or the collider index is not valid for the array of static colliders on the referenced map.</exception>
    /// <returns>A reference to the static collider hit.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ref StaticColliderData GetStaticData(FrameBase f) => ref this.GetStaticData((FrameThreadSafe) f);

    public ref StaticColliderData GetStaticData(FrameThreadSafe f) => ref f.FindAsset<Map>(this._mapGuid).StaticColliders2D[this._staticColliderIndex].StaticData;

    public static unsafe void Serialize(void* ptr, FrameSerializer serializer)
    {
      Hit* hitPtr = (Hit*) ptr;
      serializer.Stream.Serialize(&hitPtr->_isTrigger);
      serializer.Stream.Serialize(&hitPtr->ShapeUserTag);
      serializer.Stream.Serialize(&hitPtr->_staticColliderIndex);
      FP.Serialize((void*) &hitPtr->OverlapPenetration, (IDeterministicFrameSerializer) serializer);
      EntityRef.Serialize((void*) &hitPtr->_entity, serializer);
      serializer.Stream.Serialize(&hitPtr->sqrDistance);
      FPVector2.Serialize((void*) &hitPtr->Point, (IDeterministicFrameSerializer) serializer);
      FPVector2.Serialize((void*) &hitPtr->Normal, (IDeterministicFrameSerializer) serializer);
    }

    public override int GetHashCode() => (((((((17 * 31 + this._isTrigger.GetHashCode()) * 31 + this.ShapeUserTag.GetHashCode()) * 31 + this._staticColliderIndex.GetHashCode()) * 31 + this.OverlapPenetration.GetHashCode()) * 31 + this._entity.GetHashCode()) * 31 + this.sqrDistance.GetHashCode()) * 31 + this.Point.GetHashCode()) * 31 + this.Normal.GetHashCode();
  }
}